package com.wy.panda.common.util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.crypto.Cipher;

import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.wy.panda.exception.PandaCannotHandleException;

/**
 * 请求签名工具类，借鉴蚂蚁金服开放平台（也可以参考阿里巴巴开放平台签名规则）：https://docs.open.alipay.com/54/103419/
 * 
 * @author wangyuan
 * @since 2018年7月25日
 */
public class SignatureUtils {
    
    private static final Logger logger = LoggerFactory.getLogger(SignatureUtils.class);
    
    public static final String SIGN_TYPE_RSA = "RSA";

    public static final String SIGN_ALGORITHMS = "SHA1WithRSA";
    
    /**
     * 编码方式
     */
    public static final String CHARSET_UTF_8 = "UTF-8";

    /** RSA最大加密明文大小 */
    private static final int MAX_ENCRYPT_BLOCK = 117;

    /** RSA最大解密密文大小 */
    private static final int MAX_DECRYPT_BLOCK = 128;

    /**
     * 获取签名内容
     * 
     * @param params
     * @return
     */
    public static String getSignContent(Map<String, String> params) {
        StringBuffer content = new StringBuffer();
        List<String> keys = new ArrayList<String>(params.keySet());
        Collections.sort(keys);
        int index = 0;
        for (int i = 0; i < keys.size(); i++) {
            String key = keys.get(i);
            String value = params.get(key);
            if (StringUtils.areNotEmpty(key, value)) {
                content.append((index == 0 ? "" : "&") + key + "=" + value);
                index++;
            }
        }
        return content.toString();
    }

    public static boolean rsaCheckContent(String content, String sign, String publicKey, String charset)
            throws PandaCannotHandleException {
        try {
            PublicKey pubKey = getPublicKeyFromX509(SIGN_TYPE_RSA, new ByteArrayInputStream(publicKey.getBytes()));

            java.security.Signature signature = java.security.Signature.getInstance(SIGN_ALGORITHMS);

            signature.initVerify(pubKey);

            if (StringUtils.isEmpty(charset)) {
                signature.update(content.getBytes(CHARSET_UTF_8));
            } else {
                signature.update(content.getBytes(charset));
            }

            return signature.verify(Base64.decodeBase64(sign.getBytes(CHARSET_UTF_8)));
        } catch (Exception e) {
            throw new PandaCannotHandleException("RSAcontent = " + content + ",sign=" + sign + ",charset = " + charset, e);
        }
    }

    public static PublicKey getPublicKeyFromX509(String algorithm, InputStream in) throws Exception {
        KeyFactory keyFactory = KeyFactory.getInstance(algorithm);

        StringWriter writer = new StringWriter();
        StreamUtils.io(new InputStreamReader(in), writer);

        byte[] encodedKey = writer.toString().getBytes();

        encodedKey = Base64.decodeBase64(encodedKey);

        return keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));
    }
    
    /**
     * sha1WithRsa 加签
     * 
     * @param content
     * @param privateKey
     * @param charset
     * @return
     * @throws PandaCannotHandleException
     */
    public static String rsaSign(String content, String privateKey,
                                 String charset) throws PandaCannotHandleException {
        try {
            PrivateKey priKey = getPrivateKeyFromPKCS8(SIGN_TYPE_RSA, new ByteArrayInputStream(privateKey.getBytes()));

            java.security.Signature signature = java.security.Signature.getInstance(SIGN_ALGORITHMS);

            signature.initSign(priKey);

            if (StringUtils.isEmpty(charset)) {
                signature.update(content.getBytes());
            } else {
                signature.update(content.getBytes(charset));
            }

            byte[] signed = signature.sign();

            return new String(Base64.encodeBase64(signed));
        } catch (InvalidKeySpecException ie) {
            throw new PandaCannotHandleException("RSA私钥格式不正确，请检查是否正确配置了PKCS8格式的私钥", ie);
        } catch (Exception e) {
            throw new PandaCannotHandleException("RSAcontent = " + content + "; charset = " + charset, e);
        }
    }

    public static String rsaSign(Map<String, String> params, String privateKey,
                                 String charset) throws PandaCannotHandleException {
        String signContent = getSignContent(params);

        return rsaSign(signContent, privateKey, charset);
    }
    
    public static String rsaSign(Map<String, String> params, String privateKey) throws PandaCannotHandleException {
        return rsaSign(params, privateKey, CHARSET_UTF_8);
    }
    
    public static String rsaSign(String content, String privateKey) throws PandaCannotHandleException {
        return rsaSign(content, privateKey, CHARSET_UTF_8);
    }

    /**
     * 公钥加密
     * 
     * @param content 待加密内容
     * @param publicKey 公钥
     * @param charset 字符集，如UTF-8, GBK, GB2312
     * @return 密文内容
     * @throws PandaCannotHandleException
     */
    public static String rsaEncrypt(String content, String publicKey, String charset) throws PandaCannotHandleException {
        try {
            PublicKey pubKey = getPublicKeyFromX509(SIGN_TYPE_RSA, new ByteArrayInputStream(publicKey.getBytes()));
            Cipher cipher = Cipher.getInstance(SIGN_TYPE_RSA);
            cipher.init(Cipher.ENCRYPT_MODE, pubKey);
            byte[] data = StringUtils.isEmpty(charset) ? content.getBytes() : content.getBytes(charset);
            int inputLen = data.length;
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            int offSet = 0;
            byte[] cache;
            int i = 0;
            // 对数据分段加密
            while (inputLen - offSet > 0) {
                if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
                    cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK);
                } else {
                    cache = cipher.doFinal(data, offSet, inputLen - offSet);
                }
                out.write(cache, 0, cache.length);
                i++;
                offSet = i * MAX_ENCRYPT_BLOCK;
            }
            byte[] encryptedData = Base64.encodeBase64(out.toByteArray());
            out.close();

            return StringUtils.isEmpty(charset) ? new String(encryptedData) : new String(encryptedData, charset);
        } catch (Exception e) {
            throw new PandaCannotHandleException("EncryptContent = " + content + ",charset = " + charset, e);
        }
    }

    /**
     * 私钥解密
     * 
     * @param content 待解密内容
     * @param privateKey 私钥
     * @param charset 字符集，如UTF-8, GBK, GB2312
     * @return 明文内容
     * @throws PandaCannotHandleException
     */
    public static String rsaDecrypt(String content, String privateKey, String charset) throws PandaCannotHandleException {
        try {
            PrivateKey priKey = getPrivateKeyFromPKCS8(SIGN_TYPE_RSA, new ByteArrayInputStream(privateKey.getBytes()));
            Cipher cipher = Cipher.getInstance(SIGN_TYPE_RSA);
            cipher.init(Cipher.DECRYPT_MODE, priKey);
            byte[] encryptedData = StringUtils.isEmpty(charset) ? Base64.decodeBase64(content.getBytes())
                    : Base64.decodeBase64(content.getBytes(charset));
            int inputLen = encryptedData.length;
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            int offSet = 0;
            byte[] cache;
            int i = 0;
            // 对数据分段解密
            while (inputLen - offSet > 0) {
                if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
                    cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK);
                } else {
                    cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
                }
                out.write(cache, 0, cache.length);
                i++;
                offSet = i * MAX_DECRYPT_BLOCK;
            }
            byte[] decryptedData = out.toByteArray();
            out.close();

            return StringUtils.isEmpty(charset) ? new String(decryptedData) : new String(decryptedData, charset);
        } catch (Exception e) {
            throw new PandaCannotHandleException("EncodeContent = " + content + ",charset = " + charset, e);
        }
    }

    public static PrivateKey getPrivateKeyFromPKCS8(String algorithm, InputStream ins) throws Exception {
        if (ins == null || StringUtils.isEmpty(algorithm)) {
            return null;
        }

        KeyFactory keyFactory = KeyFactory.getInstance(algorithm);

        byte[] encodedKey = StreamUtils.readText(ins).getBytes();

        encodedKey = Base64.decodeBase64(encodedKey);

        return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(encodedKey));
    }
    
    /**
     * 根据种子生成公匙和私匙
     * @param seed 可为空
     * @throws Exception
     */
    public static void generateKeyOfRsa(String seed) throws Exception {
        if (StringUtils.isEmpty(seed)) {
            seed = UUID.randomUUID().toString().replaceAll("-", "");
        }

        KeyPairGenerator generator = KeyPairGenerator.getInstance(SIGN_TYPE_RSA);
        
        // 种子
        SecureRandom random = new SecureRandom(seed.getBytes(CHARSET_UTF_8));
        generator.initialize(2048, random);

        KeyPair keyPair = generator.generateKeyPair();

        PublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        PrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();

        byte[] publicKeyData = publicKey.getEncoded();
        byte[] privateKeyData = privateKey.getEncoded();

        logger.info("seed：{}", seed);
        logger.info("publicKey：{}", java.util.Base64.getEncoder().encodeToString(publicKeyData));
        logger.info("privateKey：{}", java.util.Base64.getEncoder().encodeToString(privateKeyData));
    }
}
